﻿/*                       NOTICE
 * 
 * This software was produced for the U. S. Government
 * under Contract No. FA8721-10-C-0001, and is
 * subject to the Rights in Noncommercial Computer Software
 * and Noncommercial Computer Software Documentation Clause
 * (DFARS) 252.227-7014 (JUN 1995)
 * 
 * Approved for Public Release: 11-2435
 * © 2011 The MITRE Corporation. All Rights Reserved. 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */



using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;

namespace GRIP
{
    /// <summary>
    /// ConfigParser is GRIP's configuration loading tool. It takes in an XML file
    /// and parses through it, storing the settings as key/value pairs of strings.
    /// It can then be used to recall and convert any setting to a valid basic type,
    /// or automatically replace fields in any object with the corresponding XML values.
    /// </summary>
    public class ConfigParser
    {
        // This houses the actual settings themselves
        private Dictionary<string, string> settingLookup = new Dictionary<string, string>();
        private static string FormatErrorMessage = "\"{0}\" can't be converted to type \"{1}\"";
        private static string OverflowErrorMessage = "\"{0}\" is too small or big to fit in type \"{1}\"";
        
        /// <summary>
        /// Constructs a new ConfigParser and loads its setting map from an XML file.
        /// </summary>
        /// <param name="configFile">The XML configuration file to parse</param>
        public ConfigParser(string configFile)
        {
            using (XmlTextReader reader = new XmlTextReader(configFile))
            {
                string settingName, settingValue;

                reader.ReadToFollowing("Configuration");
                // Run through the XML, getting all of the settings
                while(reader.Read())
                    if (reader.IsStartElement() && !reader.Name.Equals("Section"))
                    {
                        settingName = reader.Name;
                        settingValue = reader.ReadElementContentAsString();
                        settingLookup.Add(settingName, settingValue);
                    }
            }
        }

        /// <summary>
        /// Looks up a property name and returns the corresponding value as a string.
        /// </summary>
        /// <param name="property">The property name to look up</param>
        /// <returns>The value stored with that name</returns>
        /// <exception cref="System.ArgumentException"></exception>
        private string lookup(string property)
        {
            string value;
            if(settingLookup.TryGetValue(property, out value))
                return value;
            throw new ArgumentException(property + " was not found in the configuration XML.");
        }

        /// <summary>
        /// Converts the string stored with the setting name to the given type.
        /// </summary>
        /// <typeparam name="T">The type to convert the string value to</typeparam>
        /// <param name="property">The name of the property to look up and convert</param>
        /// <returns>The value stored with the property name as the given type</returns>
        /// <exception cref="System.ArgumentException"></exception>
        private T Convert<T>(string property)
        {
            string value = lookup(property);
            try
            {
                return (T)System.Convert.ChangeType(lookup(property), typeof(T));
            }
            catch (FormatException)
            {
                throw new ArgumentException(String.Format(FormatErrorMessage, value, typeof(T)));
            }
            catch (OverflowException)
            {
                throw new ArgumentException(String.Format(OverflowErrorMessage, value, typeof(T)));
            }
        }

        #region Well-Defined Property Getters

        /// <summary>
        /// Returns a setting as a short.
        /// </summary>
        /// <param name="property">The name of the setting to return</param>
        /// <returns>The setting as a short</returns>
        /// <exception cref="System.ArgumentException"></exception>
        public short GetShort(string property)
        {
            return Convert<short>(property);
        }

        /// <summary>
        /// Returns a setting as a ushort.
        /// </summary>
        /// <param name="property">The name of the setting to return</param>
        /// <returns>The setting as a ushort</returns>
        /// <exception cref="System.ArgumentException"></exception>
        public ushort GetUShort(string property)
        {
            return Convert<ushort>(property);
        }

        /// <summary>
        /// Returns a setting as an int.
        /// </summary>
        /// <param name="property">The name of the setting to return</param>
        /// <returns>The setting as an int</returns>
        /// <exception cref="System.ArgumentException"></exception>
        public int GetInt(string property)
        {
            return Convert<int>(property);
        }

        /// <summary>
        /// Returns a setting as a uint.
        /// </summary>
        /// <param name="property">The name of the setting to return</param>
        /// <returns>The setting as a uint</returns>
        /// <exception cref="System.ArgumentException"></exception>
        public uint GetUInt(string property)
        {
            return Convert<uint>(property);
        }

        /// <summary>
        /// Returns a setting as a long.
        /// </summary>
        /// <param name="property">The name of the setting to return</param>
        /// <returns>The setting as a long</returns>
        /// <exception cref="System.ArgumentException"></exception>
        public long GetLong(string property)
        {
            return Convert<long>(property);
        }

        /// <summary>
        /// Returns a setting as a ulong.
        /// </summary>
        /// <param name="property">The name of the setting to return</param>
        /// <returns>The setting as a ulong</returns>
        /// <exception cref="System.ArgumentException"></exception>
        public ulong GetULong(string property)
        {
            return Convert<ulong>(property);
        }

        /// <summary>
        /// Returns a setting as a bool.
        /// </summary>
        /// <param name="property">The name of the setting to return</param>
        /// <returns>The setting as a bool</returns>
        /// <exception cref="System.ArgumentException"></exception>
        public bool GetBool(string property)
        {
            return Convert<bool>(property);
        }

        /// <summary>
        /// Returns a setting as a byte.
        /// </summary>
        /// <param name="property">The name of the setting to return</param>
        /// <returns>The setting as a byte</returns>
        /// <exception cref="System.ArgumentException"></exception>
        public byte GetByte(string property)
        {
            return Convert<byte>(property);
        }

        /// <summary>
        /// Returns a setting as a float.
        /// </summary>
        /// <param name="property">The name of the setting to return</param>
        /// <returns>The setting as a float</returns>
        /// <exception cref="System.ArgumentException"></exception>
        public float GetFloat(string property)
        {
            return Convert<float>(property);
        }

        /// <summary>
        /// Returns a setting as a double.
        /// </summary>
        /// <param name="property">The name of the setting to return</param>
        /// <returns>The setting as a double</returns>
        /// <exception cref="System.ArgumentException"></exception>
        public double GetDouble(string property)
        {
            return Convert<double>(property);
        }

        /// <summary>
        /// Returns a setting as a decimal.
        /// </summary>
        /// <param name="property">The name of the setting to return</param>
        /// <returns>The setting as a decimal</returns>
        /// <exception cref="System.ArgumentException"></exception>
        public decimal GetDecimal(string property)
        {
            return Convert<decimal>(property);
        }

        /// <summary>
        /// Returns a setting as a string.
        /// </summary>
        /// <param name="property">The name of the setting to return</param>
        /// <returns>The setting as a string</returns>
        /// <exception cref="System.ArgumentException"></exception>
        public string GetString(string property)
        {
            return lookup(property);
        }

        /// <summary>
        /// Looks up a setting, and attempts to convert its value to a member of the given enum.
        /// </summary>
        /// <typeparam name="T">The enum to use</typeparam>
        /// <param name="property">The name of the setting to return</param>
        /// <returns>The setting as a member of the given enum</returns>
        /// <exception cref="System.ArgumentException"></exception>
        public T GetEnum<T>(string property)
        {
            Type t = typeof(T);
            if (t.IsEnum)
            {
                string[] enumNames = Enum.GetNames(t);
                Array enumValues = Enum.GetValues(t);
                string propertyValue = lookup(property);

                for (int i = 0; i < enumNames.Length; i++)
                    if (enumNames[i].Equals(propertyValue))
                        return (T)enumValues.GetValue(i);

                throw new ArgumentException(propertyValue + " is not a valid enum value of " + t.ToString());
            }

            throw new ArgumentException(t.ToString() + " is not an enum");
        }
        #endregion

        /// <summary>
        /// Gets a configuration setting and returns it as the properly converted value.
        /// </summary>
        /// <typeparam name="T">The type to convert the setting to</typeparam>
        /// <param name="property">The name of the setting as it appears in the XML</param>
        /// <returns>The setting as the proper type T</returns>
        /// <exception cref="System.ArgumentException"></exception>
        public T Get<T>(string property)
        {
            T val;
            Replace(property, out val);
            return val;
        }

        /// <summary>
        /// Replaces a setting's old value with the value defined in the XML.
        /// </summary>
        /// <typeparam name="T">The type to of this setting</typeparam>
        /// <param name="property">The name of the XML element to use</param>
        /// <param name="value">The value of that XML element</param>
        /// <exception cref="System.ArgumentException"></exception>
        public void Replace<T>(string property, out T value)
        {
            Type t = typeof(T);
            
            if (t == typeof(Int16))
                value = (T)(GetShort(property) as object);
            else if (t == typeof(UInt16))
                value = (T)(GetUShort(property) as object);
            else if (t == typeof(Int32))
                value = (T)(GetInt(property) as object);
            else if (t == typeof(UInt32))
                value = (T)(GetUInt(property) as object);
            else if (t == typeof(Int64))
                value = (T)(GetLong(property) as object);
            else if (t == typeof(UInt64))
                value = (T)(GetULong(property) as object);
            else if (t == typeof(Boolean))
                value = (T)(GetBool(property) as object);
            else if (t == typeof(Byte))
                value = (T)(GetByte(property) as object);
            else if (t == typeof(Single))
                value = (T)(GetFloat(property) as object);
            else if (t == typeof(Double))
                value = (T)(GetDouble(property) as object);
            else if (t == typeof(Decimal))
                value = (T)(GetDecimal(property) as object);
            else if (t == typeof(String))
                value = (T)(GetString(property) as object);
            else if (t.IsEnum)
                value = GetEnum<T>(property);
            else
                throw new ArgumentException(t.ToString() + " is an invalid property type. " +
                    "Allowed types are short, ushort, int, uint, long, ulong, bool, byte, float, double, decimal, string, and enum.");
        }

        /// <summary>
        /// A convenience function using reflection. Looks at all of the object's fields with
        /// GRIP's [Setting] attribute and dynamically converts and replaces them with the XML.
        /// If the object implements ISettingsManager, it will do a replacement on everything
        /// without the need to add the [Setting] attribute to each field.
        /// </summary>
        /// <param name="settingsHolder">The object to run setting replacement on</param>
        /// <exception cref="System.ArgumentException"></exception>
        public void LoadConfiguration(object settingsHolder)
        {
            bool classSupportsBlanketAssignment = // This class is just a holder for settings so replace everything
                typeof(ISettingManager).IsAssignableFrom(settingsHolder.GetType());

            FieldInfo[] classFields = settingsHolder.GetType().GetFields();
            foreach (FieldInfo info in classFields)
                if (classSupportsBlanketAssignment ||
                    info.GetCustomAttributes(typeof(Setting), true).Count() > 0) // Field has the [Setting] attribute
                {
                    string value;
                    if (settingLookup.TryGetValue(info.Name, out value) && value != null)
                    {
                        Type t = info.FieldType;
                        try
                        {
                            object convertedValue = null;

                            if (t.IsEnum)
                            {
                                string[] enumNames = Enum.GetNames(t);
                                Array enumValues = Enum.GetValues(t);

                                for (int i = 0; i < enumNames.Length; i++)
                                    if (enumNames[i].Equals(value))
                                        convertedValue = System.Convert.ChangeType(enumValues.GetValue(i), t);
                            }
                            else
                                convertedValue = System.Convert.ChangeType(value, t);
                            info.SetValue(settingsHolder, convertedValue);
                        }
                        catch (InvalidCastException)
                        {
                            throw new ArgumentException("Error loading " + info.Name + ": " + info.FieldType + " is an invalid property type. " +
                                "Allowed types are short, ushort, int, uint, long, ulong, bool, byte, float, double, decimal, string, and enum.");
                        }
                        catch (FormatException)
                        {
                            throw new ArgumentException("Error loading " + info.Name + ": " + String.Format(FormatErrorMessage, value, t));
                        }
                        catch (OverflowException)
                        {
                            throw new ArgumentException("Error loading " + info.Name + ": " + String.Format(OverflowErrorMessage, value, t));
                        }
                        catch (ArgumentNullException)
                        {
                            throw new ArgumentException("Error loading " + info.Name + ": " + "\"" + value + "\" is not a member of enum " + t);
                        }
                    }
                }
        }
    }
}
